﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.ModelConfiguration.Conventions;
using System.Data;
using Newtonsoft.Json;
using Disco.Core;
using System.Data.Entity.ModelConfiguration;

namespace Disco.Ontology
{
    public class OntologyContext : DbContext
    {
        public DbSet<Culture> Cultures { get; set; }
        public DbSet<Region> Regions { get; set; }

        public DbSet<Origin> Origins { get; set; }
        public DbSet<Originator> Originators { get; set; }

        public DbSet<Descriptor> Descriptors { get; set; }

        public DbSet<User> Users { get; set; }
        public DbSet<Group> Groups { get; set; }
        public DbSet<GroupMembership> GroupMemberships { get; set; }
        public DbSet<GroupMembershipType> GroupMembershipTypes { get; set; }

        public DbSet<Post> Posts { get; set; }
        public DbSet<PostReference> PostReferences { get; set; }
        public DbSet<PostReferenceType> PostReferenceTypes { get; set; }
        public DbSet<PostType> PostTypes { get; set; }

        public DbSet<Tag> Tags { get; set; }
        public DbSet<Rating> Ratings { get; set; }

        public DbSet<Content> Content { get; set; }


        [EntitySetAccessibility(AccessScope.Public)]
        public DbSet<Changeset> Changesets { get; set; }
        [EntitySetAccessibility(AccessScope.Public)]
        public DbSet<ChangeDetail> ChangeDetails { get; set; }

        [EntitySetAccessibility(AccessScope.Public)]
        public DbSet<NamedValueSet> NamedValueSets { get; set; }
        [EntitySetAccessibility(AccessScope.Public)]
        public DbSet<NamedValue> NamedValues { get; set; }


        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();

            // Culture
            var cultureConfig = modelBuilder.Entity<Culture>();
            cultureConfig.Property(culture => culture.Code)
                .IsRequired();


            // Region
            var regionConfig = modelBuilder.Entity<Region>();
            regionConfig.Property(region => region.Code)
                .IsRequired();
            regionConfig.HasRequired(region => region.Description)
                .WithMany()
                .WillCascadeOnDelete(false);
            regionConfig.HasMany(region => region.Partitions)
                .WithOptional(region => region.Parent);
            regionConfig.HasOptional(region => region.Parent)
                .WithMany()
                .WillCascadeOnDelete(false);


            // Descriptor
            var descrtiptorConfig = modelBuilder.Entity<Descriptor>();
            descrtiptorConfig.Property(descriptor => descriptor.Name)
                .IsRequired();
            descrtiptorConfig.HasRequired(descriptor => descriptor.Culture)
                .WithMany()
                .WillCascadeOnDelete(false);


            // Content
            var contentConfig = modelBuilder.Entity<Content>();
            contentConfig.Property(content => content.Text)
                .IsRequired();
            contentConfig.HasRequired(content => content.Culture)
                .WithMany()
                .WillCascadeOnDelete(false);


            // Post
            var postConfig = modelBuilder.Entity<Post>();
            postConfig.HasRequired(post => post.PostType)
                .WithMany()
                .WillCascadeOnDelete(false);
            postConfig.HasRequired(post => post.Content)
                .WithMany()
                .WillCascadeOnDelete(false);
            postConfig.HasMany(post => post.Tags)
                .WithMany(tag => tag.Tagged);
            postConfig.HasMany(post => post.RefersTo)
                .WithRequired(post => post.Referrer); // ??
            postConfig.HasMany(post => post.ReferredFrom)
                .WithRequired(post => post.Referree); // ??
            postConfig.HasMany(post => post.Ratings)
                .WithRequired(rating => rating.Post)
                .WillCascadeOnDelete(true);


            // Rating
            var ratingConfig = modelBuilder.Entity<Rating>();
            ratingConfig.HasRequired(rating => rating.Post)
                .WithMany(post => post.Ratings)
                .WillCascadeOnDelete(true);


            // PostType
            var postTypeConfig = modelBuilder.Entity<PostType>();
            postTypeConfig.HasRequired(postType => postType.Description)
                .WithMany()
                .WillCascadeOnDelete(false);


            // PostReference
            var postReferenceConfig = modelBuilder.Entity<PostReference>();
            postReferenceConfig.HasRequired(postReference => postReference.ReferenceType)
                .WithMany()
                .WillCascadeOnDelete(false);
            postReferenceConfig.HasRequired(postReference => postReference.Referree)
                .WithMany(post => post.ReferredFrom)
                .WillCascadeOnDelete(false);
            postReferenceConfig.HasRequired(postReference => postReference.Referrer)
                .WithMany(post => post.RefersTo)
                .WillCascadeOnDelete(false);


            // PostReferenceType
            var postReferneceTypeConfig = modelBuilder.Entity<PostReferenceType>();
            postReferneceTypeConfig.HasRequired(postReferenceType => postReferenceType.Description)
                .WithMany()
                .WillCascadeOnDelete(false);


            // Tag
            var tagConfig = modelBuilder.Entity<Tag>();
            tagConfig.HasRequired(tag => tag.Description)
                .WithMany()
                .WillCascadeOnDelete(false);
            tagConfig.HasMany(tag => tag.Tagged)
                .WithMany(post => post.Tags);
            tagConfig.HasMany(tag => tag.Related)
                .WithMany()
                .Map(map => {
                    map.MapLeftKey("Tag_Id");
                    map.MapRightKey("RelatedTag_Id");
                    map.ToTable("RelatedTags");
                });


            // Origin
            var originConfig = modelBuilder.Entity<Origin>();
            originConfig.Property(origin => origin.Uri)
                .IsRequired();
            originConfig.HasRequired(origin => origin.Description)
                .WithMany()
                .WillCascadeOnDelete(false);


            // Originator
            var originatorConfig = modelBuilder.Entity<Originator>();
            originatorConfig.HasRequired(originator => originator.Author)
                .WithMany()
                .WillCascadeOnDelete(true);
            originatorConfig.HasRequired(originator => originator.Origin)
                .WithMany()
                .WillCascadeOnDelete(true);


            // User
            var userConfig = modelBuilder.Entity<User>();
            userConfig.Property(user => user.Alias)
                .IsRequired();
            userConfig.Property(user => user.Token)
                .IsRequired();
            userConfig.HasRequired(user => user.Origin)
                .WithMany()
                .WillCascadeOnDelete(false);
            userConfig.HasMany(user => user.Memberships)
                .WithRequired(groupMembership => groupMembership.User);
            

            // Group
            var groupConfig = modelBuilder.Entity<Group>();
            groupConfig.Property(group => group.Alias)
                .IsRequired();
            groupConfig.HasRequired(group => group.Description)
                .WithMany()
                .WillCascadeOnDelete(false);
            groupConfig.HasMany(group => group.Memberships)
                .WithRequired(groupMembership => groupMembership.Group);


            // GroupMembership
            var groupMembershipConfig = modelBuilder.Entity<GroupMembership>();
            groupMembershipConfig.HasRequired(groupMembership => groupMembership.MembershipType)
                .WithMany()
                .WillCascadeOnDelete(false);
            groupMembershipConfig.HasRequired(groupMembership => groupMembership.User)
                .WithMany(user => user.Memberships)
                .WillCascadeOnDelete(false);
            groupMembershipConfig.HasRequired(groupMembership => groupMembership.Group)
                .WithMany(group => group.Memberships)
                .WillCascadeOnDelete(false);


            // GroupMembershipType
            var groupMembershipTypeConfig = modelBuilder.Entity<GroupMembershipType>();
            groupMembershipTypeConfig.HasRequired(groupMembershipType => groupMembershipType.Description)
                .WithMany()
                .WillCascadeOnDelete(false);


            // ChangeDetail
            var changeDetailConfig = modelBuilder.Entity<ChangeDetail>();
            changeDetailConfig.Property(changeDetail => changeDetail.Action)
                .IsRequired();
            changeDetailConfig.Property(changeDetail => changeDetail.EntityKey)
                .IsRequired();
            changeDetailConfig.Property(changeDetail => changeDetail.EntityName)
                .IsRequired();
            changeDetailConfig.Property(changeDetail => changeDetail.OldValues)
                .IsRequired()
                .HasColumnType("xml");
            changeDetailConfig.Property(changeDetail => changeDetail.NewValues)
                .IsRequired()
                .HasColumnType("xml");
            changeDetailConfig.HasRequired(changeDetail => changeDetail.Changeset)
                .WithMany(changeset => changeset.Details)
                .WillCascadeOnDelete(true);


            // Changeset
            var changesetConfig = modelBuilder.Entity<Changeset>();
            changesetConfig.HasRequired(changeset => changeset.ModifiedBy)
                .WithMany()
                .WillCascadeOnDelete(false);
            changesetConfig.HasMany(changeset => changeset.Details)
                .WithRequired(changeDetail => changeDetail.Changeset)
                .WillCascadeOnDelete(true);


            // NamedValue
            var namedValueConfig = modelBuilder.Entity<NamedValue>();
            namedValueConfig.Property(namedValue => namedValue.Name)
                .IsRequired();
            namedValueConfig.Property(namedValue => namedValue.Value)
                .IsRequired();
            namedValueConfig.HasRequired(namedValue => namedValue.NamedValueSet)
                .WithMany()
                .WillCascadeOnDelete(false);


            // NamedValueSet
            var namedValueSetConfig = modelBuilder.Entity<NamedValueSet>();
            namedValueSetConfig.HasRequired(namedValueSet => namedValueSet.Origin)
                .WithMany()
                .WillCascadeOnDelete(false);
            namedValueSetConfig.HasMany(namedValueSet => namedValueSet.Values)
                .WithRequired(namedValue => namedValue.NamedValueSet)
                .WillCascadeOnDelete(true);


            // TODO: Try to make this method calls somehow transient, e.g. by iterrating over all entities, because it's implicit knowledge.
            ExtendEntityWithTracking<Post>(postConfig);
            ExtendEntityWithTracking<Content>(contentConfig);
            ExtendEntityWithTracking<Rating>(ratingConfig);
        }

        private void ExtendEntityWithTracking<T>(EntityTypeConfiguration<T> entityConfig)
            where T: class, ITrackingEntity
        {
            entityConfig.HasRequired(entity => entity.ModifiedBy)
                .WithMany()
                .WillCascadeOnDelete(false);
            entityConfig.HasRequired(entity => entity.ChangeDetail)
                .WithMany()
                .WillCascadeOnDelete(false);
        }

        private Originator _originator = null;
        public void SetCurrentOriginator(Originator originator)
        {
            _originator = originator;
        }

        public override int SaveChanges()
        {
            var changeEntries = ChangeTracker.Entries().Where(entry =>
                entry.Entity is IEntity && (
                entry.State == EntityState.Added ||
                entry.State == EntityState.Deleted ||
                entry.State == EntityState.Modified));

            if (changeEntries.Count() > 0)
            {
                if (null == _originator)
                {
                    throw new InvalidOperationException("The current originator could not be found. On each changeset an originator must be set!");
                }

                var changeset = new Changeset
                {
                    ModifiedBy = _originator,
                    Modified = DateTime.UtcNow,
                    Key = Guid.NewGuid()
                };

                // Get all Added/Deleted/Modified entities (not Unmodified or Detached)
                foreach (var entry in changeEntries)
                {
                    ((IEntity)entry.Entity).Modified = DateTime.UtcNow;

                    if (entry.State == EntityState.Added)
                    {
                        ((IEntity)entry.Entity).Key = Guid.NewGuid();
                    }

                    var detail = AddChangeDetail(changeset, entry);

                    if (entry.Entity is ITrackingEntity)
                    {
                        ((ITrackingEntity)entry.Entity).ModifiedBy = changeset.ModifiedBy;
                        ((ITrackingEntity)entry.Entity).ChangeDetail = detail;
                    }
                }

                Changesets.Add(changeset); 
            }

            // Call the original SaveChanges(), which will save both the changes made and the audit records.
            return base.SaveChanges();
        }

        private ChangeDetail AddChangeDetail(Changeset changeset, DbEntityEntry entry)
        {
            var entityType = entry.Entity.GetType();

            var detail = new ChangeDetail {
                Key = Guid.NewGuid(),
                Modified = changeset.Modified, 
                Changeset = changeset,
                Action = entry.State.ToString(),
                EntityName = entityType.AssemblyQualifiedName, 
            };

            if (entry.State == EntityState.Added)
            {
                detail.OldValues = String.Empty;
                detail.NewValues = JsonConvert.SerializeObject(entry.CurrentValues.ToObject());
                detail.EntityKey = entry.CurrentValues.GetValue<Guid>("Key");
            }
            else if(entry.State == EntityState.Deleted)
            {
                detail.OldValues = JsonConvert.SerializeObject(entry.OriginalValues.ToObject());
                detail.NewValues = String.Empty;
                detail.EntityKey = entry.OriginalValues.GetValue<Guid>("Key");
            }
            else if (entry.State == EntityState.Modified)
            {
                detail.OldValues = JsonConvert.SerializeObject(entry.OriginalValues.ToObject());
                detail.NewValues = JsonConvert.SerializeObject(entry.CurrentValues.ToObject());
                detail.EntityKey = entry.CurrentValues.GetValue<Guid>("Key");
            }
            
            changeset.Details.Add(detail);

            return detail;
        }
    }
}